Skip to content

feat(server-utils): Add tracingChannel-to-span binding#21641

Open
logaretm wants to merge 5 commits into
developfrom
awad/bind-tracing-channel
Open

feat(server-utils): Add tracingChannel-to-span binding#21641
logaretm wants to merge 5 commits into
developfrom
awad/bind-tracing-channel

Conversation

@logaretm

@logaretm logaretm commented Jun 18, 2026

Copy link
Copy Markdown
Member

This abstracts away our tracing channels consumption into a utility that:

  • Activates a span for the tracing channel lifecycle
  • Manages the span across the entire lifecycle
    • Sets errors on the error channel
    • Correctly ends the span at the correct timing whether the traced call is sync or async
  • Allows span/error enrichment and overrides

This util can be consumed uniformly across all of our server-side runtimes, I will create a stacked PR that refactors our code to use it, showcasing Nitro, Redis, and Deno adoption of that util.

API

// activates, and manages span across the trace lifecycle
const binding = bindTracingChannelToSpan(
  dc.tracingChannel('whatever'),
  data => {
    return startInactiveSpan('spanName', {
      // attrs..
    });
  },
  {
    // How much of the lifecycle should be managed
    // - 'auto' means span ending/errors are managed with context propagation
    // - 'manual' nothing is managed, only the context propagation is applied
    lifecycle: 'auto',

    beforeSpanEnd: (span, data) => {
      // Enrich the span or execute any related logic
      // Doesn't run if `manual` lifecycle is selected
    },

    /**
     * captures errors on the `error` channel, DB instrumentations should turn it off porbably
     * doesn't have an effect if lifecycle is `manual`.
     */
    captureError: true,
  },
);

Lifecycle Management

After testing across several node releases (20 -> 26), I locked down the strategy that we can rely on to make this reliable and predictable.

  1. always start at start lifecycle event, this is emitted always and is reliable.
  2. Use error For capturing exceptions and setting error span status, always reliable.
  3. When end is emitted:
    1. If error is present on the context object, end the span immediatly, there won’t be any async events coming up, the function threw in the sync part, so there is no async part to execute.
    2. If result is present on the context object, end the span immediately, there won’t be any async events coming up. The function returned in the sync part or is sync trace itself. Use in or hasOwnProperty , Do not use undefined checks as the function can return nothing.
    3. Otherwise NO-OP, there is an async lifecycle events coming up.
  4. When asyncStart is emitted: Do nothing, this event has no value for us.
  5. When asyncEnd is emitted: Just end the span.

@logaretm logaretm force-pushed the awad/bind-tracing-channel branch from 8c4ca47 to 61a560f Compare June 18, 2026 17:56
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 27.45 kB - -
@sentry/browser - with treeshaking flags 25.88 kB - -
@sentry/browser (incl. Tracing) 45.94 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 47.7 kB - -
@sentry/browser (incl. Tracing, Profiling) 50.73 kB - -
@sentry/browser (incl. Tracing, Replay) 85.14 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 74.73 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 89.83 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 102.49 kB - -
@sentry/browser (incl. Feedback) 44.62 kB - -
@sentry/browser (incl. sendFeedback) 32.25 kB - -
@sentry/browser (incl. FeedbackAsync) 37.38 kB - -
@sentry/browser (incl. Metrics) 28.52 kB - -
@sentry/browser (incl. Logs) 28.76 kB - -
@sentry/browser (incl. Metrics & Logs) 29.45 kB - -
@sentry/react 29.25 kB - -
@sentry/react (incl. Tracing) 48.24 kB - -
@sentry/vue 32.61 kB - -
@sentry/vue (incl. Tracing) 47.8 kB - -
@sentry/svelte 27.48 kB - -
CDN Bundle 29.84 kB - -
CDN Bundle (incl. Tracing) 47.85 kB - -
CDN Bundle (incl. Logs, Metrics) 31.39 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.19 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 70.7 kB - -
CDN Bundle (incl. Tracing, Replay) 85.21 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.48 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 91.05 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.31 kB - -
CDN Bundle - uncompressed 88.8 kB - -
CDN Bundle (incl. Tracing) - uncompressed 144.84 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 93.5 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 148.81 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 218.33 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 263.7 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 267.66 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 277.4 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 281.35 kB - -
@sentry/nextjs (client) 50.63 kB - -
@sentry/sveltekit (client) 46.33 kB - -
@sentry/core/server 75.89 kB +0.06% +42 B 🔺
@sentry/core/browser 63.03 kB +0.03% +14 B 🔺
@sentry/node-core 61.7 kB +0.13% +78 B 🔺
@sentry/node 123.97 kB +0.07% +80 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 70.05 kB - -
@sentry/node/light 50.6 kB +0.11% +53 B 🔺
@sentry/node - without tracing 74.18 kB +0.08% +54 B 🔺
@sentry/aws-serverless 85.3 kB +0.08% +62 B 🔺
@sentry/cloudflare (withSentry) - minified 173.12 kB +0.14% +226 B 🔺
@sentry/cloudflare (withSentry) 432.9 kB +0.1% +427 B 🔺

View base workflow run

@logaretm logaretm marked this pull request as ready for review June 19, 2026 13:24
@logaretm logaretm requested a review from a team as a code owner June 19, 2026 13:24
@logaretm logaretm requested review from JPeer264 and andreiborza and removed request for a team June 19, 2026 13:24
@logaretm logaretm force-pushed the awad/bind-tracing-channel branch from 2fbdda3 to 3cb7959 Compare June 19, 2026 13:24
// Presence checks because caller can return `undefined` result or throw a falsy value.
if ('error' in data || 'result' in data) {
endBoundSpan(data, beforeSpanEnd);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sync end closes streaming spans early

Medium Severity

In auto lifecycle mode, the end handler ends the bound span whenever 'result' in data. Orchestrion-style channels (documented for mysql in this repo) can publish end with a result that is only an in-flight handle (e.g. a streamable Query emitter) while the operation continues with no asyncEnd. The span is ended at synchronous end instead of when the work actually finishes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3cb7959. Configure here.

logaretm added 4 commits June 19, 2026 09:57
Add `bindTracingChannelToSpan` to generalize Node `diagnostics_channel`
TracingChannel instrumentation onto Sentry spans. It binds the active
span into async context on `start` and, in the default `auto` lifecycle,
ends the span on the canonical terminal event: `end` for synchronous
calls (detected via presence of `result`/`error` on the context object)
and `asyncEnd` for async calls, recording exceptions on `error`.

Backed by a new `getTracingChannelBinding` async context strategy hook
wired through core, node-core, opentelemetry, cloudflare, and deno.
…oSpan

`bindTracingChannelToSpan` now returns a `TracingChannelBindingHandle`
`{ channel, unbind }` instead of the channel directly. `unbind()`
unsubscribes the auto lifecycle handlers and unbinds the start store
(idempotent; a no-op when no async context binding is available), so
callers can detach a binding — useful for teardown and test isolation.
@logaretm logaretm force-pushed the awad/bind-tracing-channel branch from 3cb7959 to e34e28a Compare June 19, 2026 13:57

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e34e28a. Configure here.

// Idempotent.
expect(() => unbind()).not.toThrow();
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat PR lacks integration test

Low Severity

This feature adds bindTracingChannelToSpan and async-context binding hooks but only adds Vitest unit coverage under packages/server-utils/test. Review guidelines for feat PRs expect at least one integration or E2E test exercising the new behavior in a runtime scenario.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit e34e28a. Configure here.

…ToSpan + captureError callback

`getSpan` may now return `undefined` to opt a channel payload out entirely:
nothing is bound, no span is tracked, and the active context is left
untouched so nested operations keep parenting to the enclosing span. This
lets a single channel carry events that should reuse the enclosing span
(e.g. an agent loop's per-step events) without ending it prematurely. The
`error` handler likewise no-ops when no span was bound.

Also folds in the `captureError` callback form: pass a function to set the
ExclusiveEventHintOrCaptureContext on the captured error, so integrations
can supply their own mechanism.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant